/**
 * Copyright (C) 2010-2011 J.W.Marsden
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/
package cc.plural.jsonij.jpath;

import java.io.IOException;

import cc.plural.jsonij.ConstantUtility;
import cc.plural.jsonij.JPath;
import cc.plural.jsonij.JSONParser;
import cc.plural.jsonij.Value;
import cc.plural.jsonij.jpath.ExpressionPredicate.ExpressionPredicateCombineOperator;
import cc.plural.jsonij.jpath.ExpressionPredicate.ExpressionPredicateOperator;
import cc.plural.jsonij.jpath.ExpressionPredicate.FunctionExpressionPredicateCondition;
import cc.plural.jsonij.jpath.ExpressionPredicate.OperatorExpressionPredicateCondition;
import cc.plural.jsonij.jpath.functions.FunctionArgument;
import cc.plural.parser.BaseReaderParser;
import cc.plural.parser.ParserException;
import cc.plural.parser.ReaderParser;
import static cc.plural.jsonij.Constants.ALL_CHAR;
import static cc.plural.jsonij.Constants.AND;
import static cc.plural.jsonij.Constants.COLON_CHAR;
import static cc.plural.jsonij.Constants.COMMA_CHAR;
import static cc.plural.jsonij.Constants.CURRENT_ELEMENT_CHAR;
import static cc.plural.jsonij.Constants.EQUAL;
import static cc.plural.jsonij.Constants.EXPRESSION_CHAR;
import static cc.plural.jsonij.Constants.GREATER;
import static cc.plural.jsonij.Constants.LAST_CHAR;
import static cc.plural.jsonij.Constants.LAST_PREDICATE;
import static cc.plural.jsonij.Constants.LEFT_PARENTHESIS;
import static cc.plural.jsonij.Constants.LEFT_SQUARE_BRACKET;
import static cc.plural.jsonij.Constants.LESS;
import static cc.plural.jsonij.Constants.MINUS;
import static cc.plural.jsonij.Constants.OR;
import static cc.plural.jsonij.Constants.PERIOD_CHAR;
import static cc.plural.jsonij.Constants.REVERSE_SOLIDUS;
import static cc.plural.jsonij.Constants.RIGHT_PARENTHESIS;
import static cc.plural.jsonij.Constants.RIGHT_SQUARE_BRACKET;
import static cc.plural.jsonij.Constants.SOLIDUS_CHAR;

/**
 *
 * @author openecho
 */
public class JPathParser {

    public JPath<Component> parse(String jPath) throws IOException, ParserException {
        JPathReader target = new JPathReader(jPath);
        return parse(target);
    }

    public JPath<Component> parse(JPathReader target) throws IOException, ParserException {
        
        JPath<Component> path = new JPath<Component>();
        while (target.peek() != -1) {
            if (target.peek() == SOLIDUS_CHAR) {
                target.read();
                if(target.peek() == SOLIDUS_CHAR) {
                    target.read();
                    path.add(new SearchComponent());
                }
            } else if (target.peek() == LEFT_SQUARE_BRACKET) {
                PredicateComponent predicate = readPredicate(target);
                if (predicate != null) {
                    path.add(predicate);
                }
            } else {
                KeyComponent key = readKey(target);
                if (key != null) {
                    path.add(key);
                }
            }
        }
        return path;
    }

    public KeyComponent readKey(JPathReader target) throws IOException, ParserException {
        StringBuilder keyBuilder = new StringBuilder();
        while (true) {
            if (target.peek() == -1) {
                break;
            } else if (target.peek() == SOLIDUS_CHAR) {
                break;
            } else if (target.peek() == LEFT_SQUARE_BRACKET) {
                break;
            } else if (target.peek() == REVERSE_SOLIDUS) {
                target.read();
                keyBuilder.append((char) target.read());
            } else {
                keyBuilder.append((char) target.read());
            }
        }
        return new KeyComponent(keyBuilder.toString());
    }

    public PredicateComponent readPredicate(JPathReader target) throws IOException, ParserException {
        if (target.peek() == -1) {
            return null;
        }
        if (target.peek() == LEFT_SQUARE_BRACKET) {
            target.read();
        } else {
            throw new JPathParserException("invalidStringExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) '[', (char) target.peek());
        }

        StringBuilder predicateComponentBuilder = null;
        if(target.peek() == ALL_CHAR) {
            target.read();
            if (target.peek() == RIGHT_SQUARE_BRACKET) {
                target.read();
            }
            return new AllPredicate();
        } else if(ConstantUtility.isNumeric(target.peek())) {
            /**
             * Most likely case, first character is a digit and this is a SimpleIndex.
             **/
            predicateComponentBuilder = new StringBuilder();
            predicateComponentBuilder.append((char) target.read());
            while (ConstantUtility.isNumeric(target.peek())) {
                predicateComponentBuilder.append((char) target.read());
            }
            if (target.peek() == RIGHT_SQUARE_BRACKET) {
                target.read();
                return new SimpleIndexPredicate(Integer.parseInt(predicateComponentBuilder.toString()));
            } else if(target.peek() == COMMA_CHAR) {
                // Union Predicate
                UnionPredicate unionPredicate = new UnionPredicate();
                int index = -1;
                while(ConstantUtility.isNumeric(target.peek()) || target.peek() == COMMA_CHAR) {
                    if(target.peek() == COMMA_CHAR) {
                        index = Integer.parseInt(predicateComponentBuilder.toString());
                        unionPredicate.addIndex(index);
                        target.read();
                        predicateComponentBuilder = new StringBuilder();
                    } else if(ConstantUtility.isNumeric(target.peek())) {
                        predicateComponentBuilder.append((char) target.read());
                    }
                }
                if(target.peek() == RIGHT_SQUARE_BRACKET) {
                    index = Integer.parseInt(predicateComponentBuilder.toString());
                    unionPredicate.addIndex(index);
                    target.read();
                    return unionPredicate;
                } else {
                    throw new JPathParserException("invalidStringExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) ']', (char) target.peek());
                }
            } else if(target.peek() == COLON_CHAR) {
                // Start End Step Predicate
                int start = Integer.parseInt(predicateComponentBuilder.toString());
                int end = -1;
                int step = 1;
                
                StartEndStepPredicate startEndStepPredicate = new StartEndStepPredicate();



                if(target.peek() == RIGHT_SQUARE_BRACKET) {
                    int num = Integer.parseInt(predicateComponentBuilder.toString());
                    target.read();
                    return startEndStepPredicate;
                } else {
                    throw new JPathParserException("invalidStringExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) ']', (char) target.peek());
                }
            } else {
                throw new JPathParserException("invalidStringExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) ']', (char) target.peek());
            }
        } else if (target.peek() == LAST_CHAR) {
            /**
             * Check for $
             **/
            target.read();
            if (target.peek() == RIGHT_SQUARE_BRACKET) {
                target.read();
                return LastIndexPredicate.LAST;
            } else if (target.peek() == MINUS) {
                target.read();
                if (ConstantUtility.isDigit(target.peek())) {
                    predicateComponentBuilder = new StringBuilder();
                    predicateComponentBuilder.append((char) target.read());
                    while (ConstantUtility.isDigit(target.peek())) {
                        predicateComponentBuilder.append((char) target.read());
                    }
                    if (target.peek() == RIGHT_SQUARE_BRACKET) {
                        target.read();
                        return new LastIndexPredicate(Integer.parseInt(predicateComponentBuilder.toString()));
                    } else {
                        throw new JPathParserException("invalidStringExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) ']', (char) target.peek());
                    }
                } else {
                    throw new UnsupportedOperationException("Not a digit in LastIndex.");
                }
            } else {
                throw new JPathParserException("invalidValue", target.getLineNumber(), target.getPositionNumber(), (char) ']', (char) target.peek());
            }
        } else if (target.peek() == LAST_PREDICATE.charAt(0)) {
            /**
             * Check for last() predicate string
             **/
            for (int i = 0; i < LAST_PREDICATE.length(); i++) {
                if (target.peek() == LAST_PREDICATE.charAt(i)) {
                    target.read();
                } else {
                    throw new JPathParserException("invalidValue", target.getLineNumber(), target.getPositionNumber(), LAST_PREDICATE, (char) target.peek());
                }
            }
            if (target.peek() == RIGHT_SQUARE_BRACKET) {
                target.read();
                return LastIndexPredicate.LAST;
            } else if (target.peek() == MINUS) {
                target.read();
                if (ConstantUtility.isDigit(target.peek())) {
                    predicateComponentBuilder = new StringBuilder();
                    predicateComponentBuilder.append((char) target.read());
                    while (ConstantUtility.isDigit(target.peek())) {
                        predicateComponentBuilder.append((char) target.read());
                    }
                    if (target.peek() == RIGHT_SQUARE_BRACKET) {
                        target.read();
                        return new LastIndexPredicate(Integer.parseInt(predicateComponentBuilder.toString()));
                    } else {
                        throw new JPathParserException("invalidStringExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) ']', (char) target.peek());
                    }
                } else {
                    throw new UnsupportedOperationException("Not a digit in LastIndex.");
                }
            } else {
                throw new JPathParserException("invalidPathExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) ']', (char) target.peek());
            }
        } else if(target.peek() == COLON_CHAR) {
            // Start:End:Step
            int start = 0;
            int end = 0;
            int step = 1;

        } else if(target.peek() == EXPRESSION_CHAR) {
            ExpressionPredicate expressionPredicate = new ExpressionPredicate();

            target.read();
            if (target.peek() == LEFT_PARENTHESIS) {
                target.read();

                StringBuilder expressionStringBuilder = new StringBuilder();
                char c;
                while (target.peek() != RIGHT_PARENTHESIS) {
                    if(target.peek() == -1) {
                        throw new JPathParserException("invalidPathFoundEndExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) ')', (char) target.peek());
                    }

                    target.skipWhitepace(expressionStringBuilder);
                    
                    ExpressionPredicateCombineOperator op = null;
                    
                    if(target.peek() == AND) {
                        while(target.peek() == AND) {
                            expressionStringBuilder.append((char) target.read());
                        }
                        op = ExpressionPredicateCombineOperator.AND;
                    } else if(target.peek() == OR) {
                        while(target.peek() == OR) {
                            expressionStringBuilder.append((char) target.read());
                        }
                        op = ExpressionPredicateCombineOperator.OR;
                    }
                    target.skipWhitepace(expressionStringBuilder);
                    if (target.peek() == CURRENT_ELEMENT_CHAR) {
                        expressionStringBuilder.append((char) target.read());
                        if(target.peek() == PERIOD_CHAR) {
                            expressionStringBuilder.append((char) target.read());

                            target.skipWhitepace(expressionStringBuilder);
                    
                            String attribute = null;
                            String value = null;
                            
                            predicateComponentBuilder = new StringBuilder();

                            while(target.peek() != RIGHT_PARENTHESIS && target.peek() != LESS && target.peek() != GREATER && target.peek() != EQUAL) {
                                c = (char) target.read();
                                expressionStringBuilder.append(c);
                                predicateComponentBuilder.append(c);
                            }
                            attribute = predicateComponentBuilder.toString().trim();

                            ExpressionPredicateOperator expressionPredicateOperator = null;
                            if(target.peek() == RIGHT_PARENTHESIS) {
                                expressionStringBuilder.append((char) target.read());
                                expressionPredicateOperator = ExpressionPredicateOperator.NOT_NULL;
                            } else if(target.peek() == EQUAL) {
                                expressionStringBuilder.append((char) target.read());
                                expressionPredicateOperator = ExpressionPredicateOperator.EQUAL;
                            } else if(target.peek() == LESS) {
                                expressionStringBuilder.append((char) target.read());
                                if(target.peek() == EQUAL) {
                                    expressionStringBuilder.append((char) target.read());
                                    expressionPredicateOperator = ExpressionPredicateOperator.LESS_EQUAL;
                                } else {
                                    expressionPredicateOperator = ExpressionPredicateOperator.LESS;
                                }
                            } else if(target.peek() == GREATER) {
                                expressionStringBuilder.append((char) target.read());
                                if(target.peek() == EQUAL) {
                                    expressionStringBuilder.append((char) target.read());
                                    expressionPredicateOperator = ExpressionPredicateOperator.GREATER_EQUAL;
                                } else {
                                    expressionPredicateOperator = ExpressionPredicateOperator.GREATER;
                                }
                            }
                            target.skipWhitepace(expressionStringBuilder);
                            predicateComponentBuilder = new StringBuilder();
                            while(target.peek() != RIGHT_PARENTHESIS && target.peek() != AND && target.peek() != OR) {
                                if(target.peek() == -1) {
                                    throw new JPathParserException("invalidPathFoundEndExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) ')', (char) target.peek());
                                }
                                c = (char) target.read();
                                expressionStringBuilder.append(c);
                                predicateComponentBuilder.append(c);
                            }
                            value = predicateComponentBuilder.toString().trim();
                            JSONParser jsonParser = new JSONParser();
                            Value jsonValue = jsonParser.parseValue(value);
                            target.skipWhitepace(expressionStringBuilder);
                            OperatorExpressionPredicateCondition predicateCondition = new OperatorExpressionPredicateCondition(attribute, expressionPredicateOperator, jsonValue);
                            if(op != null) {
                                predicateCondition.setCombine(op);
                                op = null;
                            }
                            expressionPredicate.conditions().add(predicateCondition);
                        }
                    } else if(jPathFunctionParseCheck(target.peek())) {
                        
                        // Read Function Name
                        StringBuilder functionNameBuilder = new StringBuilder();
                        while(target.peek() != -1 && target.peek() != '(') {
                            c = (char) target.read();
                            expressionStringBuilder.append(c);
                            functionNameBuilder.append(c);
                        }
                        c = (char) target.read();
                        expressionStringBuilder.append(c);
                        target.skipWhitepace(expressionStringBuilder);
                        
                        // Read Function Arguments
                        StringBuilder argumentsBuilder = new StringBuilder();
                        
                        while(target.peek() != -1 && target.peek() != ')') {
                            c = (char) target.read();
                            expressionStringBuilder.append(c);
                            argumentsBuilder.append(c);
                        }
                        c = (char) target.read();
                        expressionStringBuilder.append(c);
                        
                        FunctionArgument[] argumentArray = FunctionArgument.parseStringToArguments(argumentsBuilder.toString().trim());
                        FunctionExpressionPredicateCondition predicateCondition = new FunctionExpressionPredicateCondition(functionNameBuilder.toString(), argumentArray);
                        
                        expressionPredicate.conditions().add(predicateCondition);
                        
                    } else {
                        throw new JPathParserException("expression1", 0, target.getPositionNumber(), expressionStringBuilder.toString());
                    }
                }
                // Read Left Parenthesis
                target.read();

                expressionPredicate.setExpression(expressionStringBuilder.toString());

                if(target.peek() == RIGHT_SQUARE_BRACKET) {
                    expressionStringBuilder.append((char) target.read());
                }

                return expressionPredicate;
            } else {
                throw new JPathParserException("invalidStringExpecting1", target.getLineNumber(), target.getPositionNumber(), (char) '(', (char) target.peek());
            }
        }
        throw new UnsupportedOperationException("Invalid Predicate.");
    }
    
    public boolean jPathFunctionParseCheck(int i) {
        if(i == 'r') {
            return true;
        }
        return false;
    }
    

    public static class JPathReader extends BaseReaderParser implements ReaderParser {

        String jPathString;
        int index;

        public JPathReader(String jpathString) {
            this.jPathString = jpathString;
            index = 0;
        }

        public String getJPath() {
            return jPathString;
        }

        @Override
        public int readNext() throws IOException, ParserException {
            if(index < jPathString.length()) {
                return jPathString.charAt(index++);
            } else {
                return -1;
            }
            
        }

        public void skipWhitepace() throws IOException, ParserException {
            while(ConstantUtility.isWhiteSpace(peek())) {
                read();
            }
        }

        public void skipWhitepace(StringBuilder appender) throws IOException, ParserException {
            while(ConstantUtility.isWhiteSpace(peek())) {
                appender.append((char) read());
            }
        }
    }
}
